/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import cgl.narada.jms.JmsEvent;
import cgl.narada.matching.jms.ClientJmsMatching;
import cgl.narada.matching.jms.JmsMatching;
import cgl.narada.matching.jms.JmsMatchingTree;
import cgl.narada.matching.jms.JmsProfile;


/** 
   This determines if a profile change needs to be propagated to a higher 
   level node, and is responsible for doing so. In addition, a node could be
   gateway at multiple levels. Depending on the level for which profile 
   changes have taken place, the propagation needs to be done accordingly.

   @author Shrideep Pallickara
   $Revision$
   $Date$
*/

public class JmsProfilePropagation implements ProtocolDebugFlags {
  /* Gateway Information */
  private GatewayInfo gatewayInfo;
  
  /** Matching Tree Vector */
  private Vector matchingTreeVector; 
  
  /** Gateways Indicator - This provides indication regarding wether 
      this node is  a gateway at certain levels.  By default every node is a 
      level-0 gateway*/
  private boolean[] gatewayIndicator;
  
  /** Gateways at level : This provides us with a list of level-l gateways 
      within the level-l unit */
  private Vector gatewaysAtLevel;  
  private int systemLevel;

  private Destinations thisDestination;

  /** Protocol Handler */
  private ProtocolHandler protocolHandler;

  private String moduleName = "JmsProfilePropagation: ";


  public JmsProfilePropagation(GatewayInfo gatewayInfo,
			       ProtocolHandler protocolHandler) {
    this.gatewayInfo = gatewayInfo;
    systemLevel = gatewayInfo.getSystemLevel();
    /** There is a change from systemLevel+1 to systemLevel. This is to account
	for the fact that we are interested only about level-l gatekeepers 
	within the level-l unit */
    gatewaysAtLevel = new Vector();
    for (int i=0; i < systemLevel+1; i++) {
      gatewaysAtLevel.addElement(new Vector());
    }
    
    thisDestination = protocolHandler.getNodeDestination();
    /** The next step is to initialize the matching tree vector */
    matchingTreeVector = new Vector(systemLevel+1);
    
    /** Add the client matching algorithm for matching client destinations*/
    matchingTreeVector.addElement(new ClientJmsMatching(0, systemLevel, this));
    ((JmsMatchingTree)matchingTreeVector.elementAt(0)).setNodeDestination(thisDestination);
    
    for (int i=1; i <systemLevel +1; i++) {
      matchingTreeVector.addElement(new Object());
    }
    
    this.protocolHandler = protocolHandler;
    gatewayIndicator = new boolean[systemLevel+1];
    for (int i=0; i < systemLevel+1; i++) {
      gatewayIndicator[i] = false;
    }
    gatewayIndicator[0] =true;
  }
  
  
  public void setDestination(Destinations destination) {
    thisDestination = destination;
  }


  
  /** This designates this node as a gateway at level <i>level</i>. */
  public void setAsGatewayAtLevel(int level) {
    System.out.println(moduleName + "setting as gateway at level (" 
		       + level + ")");
    gatewayIndicator[level] = true;
    if (level !=0) {
      matchingTreeVector.setElementAt(new JmsMatching(level,systemLevel,this),
				      level);
      ((JmsMatchingTree)matchingTreeVector.elementAt(level)).setNodeDestination(thisDestination);
    }
  }

  
  public void processPropagationRequestFromClient(byte[] propagationRequest) {
    int offset=0;
    boolean addProfile=false;
    
    ByteArrayInputStream baInputStream = 
      new ByteArrayInputStream(propagationRequest);
    DataInputStream din = new DataInputStream(baInputStream);
    try {
      if (din.readByte() == Protocol.JMS_CLIENT_ADD_PROFILE) {
	addProfile = true;
      }
      int profileLength = din.readInt();
      byte[] profileBytes = new byte[profileLength];
      din.readFully(profileBytes);
      JmsProfile jmsProfile = new JmsProfile(profileBytes);
      if (JmsProfilePropagation_Debug) {
	System.out.println(moduleName + "Received " + jmsProfile);
      }
      
      JmsMatchingTree matching = 
	(JmsMatchingTree)matchingTreeVector.elementAt(0);
      
      if (addProfile) {
	matching.addSubscriptionProfile(jmsProfile);
      } else {
	matching.removeSubscriptionProfile(jmsProfile);
      }
      matching.propagateChangesToHigherLevels(jmsProfile, addProfile); 
    } catch(IOException ioe) {
      System.out.println(moduleName + "Error during unmarshalling " + ioe);
    } /* end try-catch */

  }

  /** This method indicates if the node in question is a gateway at the
      level in the argument 
      @param level The level for which we are testing if the node is a 
      gateway.
  */
  public boolean isGatewayAtLevel(int level) {
    return gatewayIndicator[level];
  }

  
  /** This method is called only if the isGatewayAtLevel returns <i>true</i>.
      This method computes the destinations associated with the matching of
      an event at a particular level 
      @param jmsEvent The event that needs to be matched.
      @param level The destinations at level (<i>level-1</i>) that we are
      interested in.
      @return The destinations that are associated with the event.
  */
  public int computeDestinationsAtLevel(JmsEvent jmsEvent, 
					int level) {
    if (!gatewayIndicator[level+1]) {
      System.out.println(moduleName + 
			 "JmsPP:computeDestinationsAtLevel-> Wrong invocation"
			 + " of the method for computing destinations at level"
			 + "(" +level+")");
      return 0;      
    }
    
    JmsMatching matchingTree = 
      (JmsMatching) matchingTreeVector.elementAt(level+1);
    return matchingTree.matchEvent(jmsEvent);
  }


  public Hashtable computeClientDestinations(JmsEvent jmsEvent) {
    ClientJmsMatching clientMatching = 
      (ClientJmsMatching) matchingTreeVector.elementAt(0);
    return clientMatching.matchEvent(jmsEvent);
  }



  /** This method is called only if the isGatewayAtLevel returns <i>true</i>.
      This method computes the destinations associated with the matching of
      an event at a particular level. This method invokes the matching event
      method which returns the profiles/destination along with the 
      destinations i.e the eventDestinations type. This is then written to
      stable storage.
      
      @param jmsEvent The event that needs to be matched.
      @param level The destinations at level (<i>level-1</i>) that we are
      interested in.
      @return The destinations that are associated with the event.
  */
  public cgl.narada.matching.EventDestinations 
  computeStorageDestinationsAtLevel(JmsEvent jmsEvent, int level) {
    if (!gatewayIndicator[level+1]) {
      System.out.println(moduleName +
			 "JmsPP:computeDestinationsAtLevel=>Wrong invocation" +
			 " of the method for computing destinations at level" +
			 "(" +level+")");
      return  new cgl.narada.matching.EventDestinations();
    }
    /*
      JmsMatching matchingTree = 
       (JmsMatching) matchingTreeVector.elementAt(level+1);
      EventDestinations eventDest = new EventDestinations();
      matchingTree.matchEventForStorage(mEvent, eventDest);
      return eventDest; */
    return null;
  }

  
  /** This method is responsible for propagating profile changes to 
      trees at the next highest level. 
      @param jmsProfile This is the subscription profile that needs to be
      propagated.
      @param level The level for which this change is being propagated
      @param addProfile A boolean variable indicating wether this profile
      is being added to the system or is being removed.
  */
  public void propagateProfileChange(JmsProfile jmsProfile, 
				     int level, 
				     boolean addProfile) {
    System.out.println(moduleName + "level=" + level);
    /** We now have a list of the gateways that exist within a certain level*/
    Vector levelGateways =
      gatewayInfo.listTheGatewaysWithinUnitAtLevel(level);
    for (Enumeration e = levelGateways.elements(); e.hasMoreElements();) {
      Gateway _gateway = (Gateway)e.nextElement();
      /** We are converting this into destinations that need to be reached */
      System.out.println(_gateway);
      int _gLevel = _gateway.getLevel();
      Destinations destinationToReach = new Destinations(systemLevel);
      for (int i = systemLevel; i > _gLevel; i--) {
	destinationToReach.setDestinationsAtLevel(i,
						  thisDestination.getDestinationsAtLevel(i));
      }
      destinationToReach.updateDestinationList(_gateway);
      if (destinationToReach.equals(thisDestination)) {
	JmsMatchingTree _matchingTreeAtLevel =
	  (JmsMatchingTree)matchingTreeVector.elementAt(level);
	if (addProfile) 
	  _matchingTreeAtLevel.addSubscriptionProfile(jmsProfile); 
	else 
	  _matchingTreeAtLevel.removeSubscriptionProfile(jmsProfile); 
      }
      System.out.println(destinationToReach);
      disseminateProfileChange(jmsProfile, level, addProfile, 
			       destinationToReach, thisDestination);
    }
  }

  private void disseminateProfileChange(JmsProfile jmsProfile, 
					int level, boolean addProfile,
					Destinations toReach,
					Destinations reachedSoFar) {
   
    reachedSoFar.markAsTraversedAt(thisDestination);
    System.out.println(moduleName + "ToReach " + toReach + 
		       "ReachedSoFar" + reachedSoFar);
    Gateway[] hopsToTake =
      gatewayInfo.hopsToReachDestination(toReach, reachedSoFar);
    
    /** if connection.level < hop.level ... don not send over the hop */
    if (hopsToTake != null) {
      for (int i=0; i < hopsToTake.length; i++) {
	if (hopsToTake[i].getLevel() >= level) {
	  /** This hop can't be used since we are not supposed to route it
	      over a higher level hop. This scenario will not take place, and
	      should signal an error somewhere.*/
	  System.out.println(moduleName + " Hop returned has level "+
			     " greater than the level of change");
	  continue;
	}
	/** Depending on the kind of hop that you are gonna be sending the
	    connection ... send the relevant connection information over the
	    link */
	byte[] byteStream = 
	  createProfilePropagationRequest(jmsProfile, level, addProfile,
					  toReach,  reachedSoFar,
					  hopsToTake[i].getLevel());
	protocolHandler.sendToNode(hopsToTake[i], byteStream);
	System.out.println(moduleName + "change at level(" + level +
			   ") [[" + jmsProfile + 
			   "]] ==>  Byte stream sent over hop "+ 
			   hopsToTake[i]);
      }
      return;
    }
    System.out.println(moduleName + "No more hops to take");
  }
  
  
  private byte[] createProfilePropagationRequest(JmsProfile jmsProfile, 
						 int level, boolean addProfile,
						 Destinations toReach,
						 Destinations reachedSoFar,
						 int hopLevel) {
    ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
    DataOutputStream dout = new DataOutputStream(baOutputStream);
    
    try {
      if (addProfile) {
	dout.writeByte(Protocol.JMS_ADD_PROFILE);
      } else {
	dout.writeByte(Protocol.JMS_REMOVE_PROFILE);
      }
      dout.writeInt(level); /** The level of profile change */
      byte[] profileBytes = jmsProfile.getBytes();
      dout.writeInt(profileBytes.length); /** write the length of the stream */
      dout.write(profileBytes); /** write the serialized profile */
      
      /** Next write down the destinations that need to be reached */
      dout.write(toReach.prepareDestinationsToSendOverLevel(hopLevel));
      
      /** Next write down the destinations that have been reached so far */
      dout.write(reachedSoFar.prepareDestinationsToSendOverLevel(hopLevel));
      
      dout.flush();
    } catch (IOException ioe) {
      System.out.println(moduleName + "Error marshalling stream" + ioe);
    } /* end try-catch */
    return baOutputStream.toByteArray();
  }
  
  /** This method handles the profile propagation requests received over the
      protocol Handler. In some cases based on the toReach destinations that 
      are received we need to provide the destination address more accurately.
      @param propagationRequest The byte stream containing the profile 
      propagation request.
  */
  public void handleProfilePropagationRequest(byte[] propagationRequest) {
    ByteArrayInputStream baInputStream = 
      new ByteArrayInputStream(propagationRequest);
    DataInputStream din = new DataInputStream(baInputStream);
    boolean addProfile = false;
    byte addProf;
    int level; 
    byte[] toReachBytes, traversedSoFarBytes;
    JmsProfile jmsProfile;
    try {
      addProf = din.readByte(); /** indicates if this is add profile*/ 
      level = din.readInt(); /** the level for the change */
      byte[] profileBytes = new byte[din.readInt()]; 
      din.readFully(profileBytes); /* read the profile bytes */
      jmsProfile = new JmsProfile(profileBytes);

      /** Unmarshall the destinations to be reached*/
      toReachBytes = new byte[4*(systemLevel+1)];
      din.readFully(toReachBytes);
      /** Unmarshall the destinations that have been reached*/
      traversedSoFarBytes = new byte[4*(systemLevel+1)];
      din.readFully(traversedSoFarBytes);   
    } catch(IOException ioe) {
      System.out.println(moduleName + "Error during unmarshalling " + ioe);
      return;
    } /* end try-catch */

    if (addProf == Protocol.JMS_ADD_PROFILE) 
      addProfile = true;
    Destinations toReach        = new Destinations(toReachBytes);
    Destinations traversedSoFar = new Destinations(traversedSoFarBytes);
    
    if (toReach.equals(thisDestination)) {
      /** We have arrived at the destination that we needed to arrive */
      JmsMatching _matchingTreeAtLevel =
	(JmsMatching)matchingTreeVector.elementAt(level);
      if (addProfile) {
	_matchingTreeAtLevel.addSubscriptionProfile(jmsProfile); 
      } else {
	_matchingTreeAtLevel.removeSubscriptionProfile(jmsProfile);
      }
      /** No further work needs to be done, since we have updated the 
	  matching tree that we intended to update when we started off */
      return;
    }

    for (int i= systemLevel; i <= level; i--) {
      if (toReach.getDestinationsAtLevel(i) == 
	  thisDestination.getDestinationsAtLevel(i) ) {
	//continue;
	break;
      } else {
	/** We have to forward this request on to the destination that needs
	    to be reached */
	System.out.println(moduleName + "Address differs at level ("+ i +")"); 
	traversedSoFar.markAsTraversedAt(thisDestination);
	disseminateProfileChange(jmsProfile, level, addProfile, 
				 toReach, traversedSoFar);
	return;
      }
    }
    
    /* This implies we are in the right unit. We next have to see if we 
       can add to the toReach and provide a precise address for the 
       toReach destination */
    if (toReach.getDestinationsAtLevel(0) == 0) {
      /** This implies that we need to calculate the addess for the toReach.
	  We could contribute atleast one level, once we are within the
	  level unit */
      /** We now have a list of the gateways that exist within a 
	  certain level*/
      Vector levelGateways =
	gatewayInfo.listTheGatewaysWithinUnitAtLevel(level);
      for (Enumeration e = levelGateways.elements(); e.hasMoreElements();) {
	Gateway _gateway = (Gateway)e.nextElement();
	/** We are converting this into destinations that need to be reached */
	System.out.println(_gateway);
	int _gLevel = _gateway.getLevel();
	for (int i = systemLevel; i > _gLevel; i--) {
	  toReach.setDestinationsAtLevel(i,
					 thisDestination.getDestinationsAtLevel(i));
	}
	toReach.updateDestinationList(_gateway);
	if (toReach.equals(thisDestination)) {
	  JmsMatching _matchingTreeAtLevel =
	    (JmsMatching)matchingTreeVector.elementAt(level);
	  if (addProfile) 
	    _matchingTreeAtLevel.addSubscriptionProfile(jmsProfile); 
	  else 
	    _matchingTreeAtLevel.removeSubscriptionProfile(jmsProfile); 
	  return;
	}
	System.out.println(moduleName + "ToReach " +toReach);
	traversedSoFar.markAsTraversedAt(thisDestination);
	disseminateProfileChange(jmsProfile, level, addProfile, 
				 toReach, traversedSoFar);
      } /* end for */   
      return; /** We don't need to execute the set of commands below */
    } /* end if */
    System.out.println(moduleName + "ToReach " +toReach);
    traversedSoFar.markAsTraversedAt(thisDestination);
    disseminateProfileChange(jmsProfile, level, addProfile, 
			     toReach, traversedSoFar);
  }
  
  


  public void connectionToClientLost(Object clientIdentifier, String linkId) {
     if (JmsProfilePropagation_Debug) {
         System.out.println(moduleName + "Connection to [" + clientIdentifier 
		      + "] lost");
     }
    ClientJmsMatching clientMatching = 
      (ClientJmsMatching) matchingTreeVector.elementAt(0);
    clientMatching.removeProfilesForDestination(clientIdentifier, linkId);
  }

  
  public static void main(String[] args) {


  }

}
